import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * Creates a 2D graph that can scroll as it is provided
 * with new data.
 * @author Callum Hay
 */
public class Scroll2DGraph extends JPanel{
	
	private static final int TEXT_CHAR_HEIGHT = 12;
	private static final int TEXT_CHAR_WIDTH = 8;
	
	private int maxDataPts = 50;					// Maximum allowed data points plotted at a time
	private int timeCounter = 0;
	private int minY, maxY;							// The minimum and maximum values for any y-coord
	
	private boolean displayExtraXVals = false;
	
	private HashMap<String, ArrayList<Point2D>> data = null; // Hold data points in order of time
	private HashMap<String, Color> lineColours = null;
	private HashMap<String, Color> pointColours = null;
	
	private GraphPanel graph = null;		// Panel that contains the graph itself
	private PaintLabel xLabel, yLabel;		// Labels for x and y axes
	
	/**
	 * Constructor for Scroll2DGraph.
	 * @param xLabel Label for the x-axis.
	 * @param yLabel Label for the y-axis.
	 * @param maxDataPts Max number of data points that can be graphed.
	 */
	public Scroll2DGraph(String yLabel, String xLabel, int minY, int maxY) {
		assert(minY < maxY);

		// Setup the instance variables, etc.
		this.graph = new GraphPanel();
		this.xLabel = new PaintLabel(xLabel, false);
		this.yLabel = new PaintLabel(yLabel, true);

		this.data = new HashMap<String, ArrayList<Point2D>>(3);
		this.lineColours = new HashMap<String, Color>(3);
		this.pointColours = new HashMap<String, Color>(3);
		
		this.minY = minY;
		this.maxY = maxY;
		
		// Establish the layout of this component
		this.establishLayout();
	}
	
	public Scroll2DGraph(String yLabel, String xLabel, int minY, int maxY, int maxDataPts) {
		this(yLabel, xLabel, minY, maxY);
		this.maxDataPts = maxDataPts;
	}
    
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(300,110);
    }
	
	private void establishLayout() {
		this.setLayout(new BorderLayout());
		
		Panel graphAndX = new Panel();
		graphAndX.setLayout(new BorderLayout());
		graphAndX.add(this.graph, BorderLayout.CENTER);
		graphAndX.add(this.xLabel, BorderLayout.SOUTH);

		this.xLabel.setAlignment(Label.CENTER);
		
		this.add(this.yLabel, BorderLayout.WEST);
		this.add(graphAndX, BorderLayout.CENTER);	
	}
	
	public void addGraphDataLine(String label, Color ptColour, Color lineColour, ArrayList<Point2D> pts) {
		ArrayList<Point2D> ptList = new ArrayList<Point2D>(maxDataPts);
		this.data.put(label, ptList);
		this.pointColours.put(label, ptColour);
		this.lineColours.put(label, lineColour);
		for (int i = 0; i < pts.size(); i++) {
			if (i >= maxDataPts) {
				break;
			}
			ptList.add(pts.get(i));
		}
		
	}

	public void addGraphDataLine(String label, Color ptColour, Color lineColour) {
		this.data.put(label, new ArrayList<Point2D>(maxDataPts));
		this.pointColours.put(label, ptColour);
		this.lineColours.put(label, lineColour);
	}	
	
	public void setDisplayExtraXValues(boolean display) {
		this.displayExtraXVals = display;
	}
	
	/**
	 * Tick function, given data for the dependent variable in
	 * the graph it will add a data point with the given value for
	 * the next time.
	 * @param yData The value for the next time tick.
	 */
	public void tick(String label, double yData) {
		ArrayList<Point2D> labelData = this.data.get(label);
		if (labelData == null) {
			return;
		}
		labelData.add(new Point2D.Double(timeCounter, yData));
		if (labelData.size() > maxDataPts) {
			labelData.remove(0);
		}
		this.dataRepaint();
	}
	
	public void incrementTimer() {
		this.timeCounter++;
	}
	
	public void dataRepaint() {
		this.graph.repaint();
	}
	
	public void paint(Graphics g) {
		super.paint(g);
		this.graph.repaint();
	}
	
	private class PaintLabel extends Label {
		private String str;
		private boolean isVertical;
		
		public PaintLabel(String s, boolean vertical) {
			this.setText("   ");
			this.str = s;
			this.isVertical = vertical;
		}
		
		public void paint(Graphics g) {
			g.setColor(Color.BLACK);
			Graphics2D temp = (Graphics2D)g;
			if (this.isVertical) {
				temp.rotate(-Math.PI/2);
				temp.drawString(this.str, (-this.getHeight() - this.str.length()*TEXT_CHAR_WIDTH/2)/2, TEXT_CHAR_HEIGHT + 1);
			}
			else {
				temp.drawString(this.str, (this.getWidth() - this.str.length()*TEXT_CHAR_WIDTH/2)/2, TEXT_CHAR_HEIGHT - 1);
			}
		}
	}
	
	private class GraphPanel extends Panel {
		private static final double EPSILON = 0.07;
		
		private static final int POINT_RADIUS = 1;
		private static final int POINT_OUTLINE_RADIUS = POINT_RADIUS + 1;
		private int borderPaddingHeight = TEXT_CHAR_HEIGHT + 1;
		private int borderPaddingWidth = 20;
		
		private double minXValue = 0;
		private double maxXValue = 0;
		
		private String trimDecimals(int numDec, String number) {
			String temp = "" + number;

			int indexOfDec = temp.indexOf('.');
			if (indexOfDec == -1) {
				return temp;
			}
			
			return temp.substring(0, indexOfDec) + temp.substring(indexOfDec, Math.min(indexOfDec+numDec+1, temp.length()));
		}
		
		
		public void paint(Graphics g) {
			// Figure out what the border padding should be
			String minYStr = "" + minY;
			String maxYStr = "" + maxY;
			int maxSizeLabel = Math.max(minYStr.length(), maxYStr.length());
			
			borderPaddingWidth = maxSizeLabel*TEXT_CHAR_WIDTH + 1;
			
			// Find the smallest time value
			this.minXValue = Integer.MAX_VALUE;
			this.maxXValue = Integer.MIN_VALUE;
			
			for (ArrayList<Point2D> pts : data.values()) {
				if (pts.size() == 0) {
					continue;
				}
				if (pts.get(0).getX() < this.minXValue) {
					this.minXValue = pts.get(0).getX();
				}
				if (pts.get(pts.size()-1).getX() > this.maxXValue) {
					this.maxXValue = pts.get(pts.size()-1).getX();
				}
			}
			
			// Draw the axis lines
			g.setColor(Color.BLACK);
			g.drawLine(borderPaddingWidth, 0, borderPaddingWidth, this.getHeight()-borderPaddingHeight);
			g.drawLine(borderPaddingWidth, this.getHeight()-borderPaddingHeight, this.getWidth(), this.getHeight()-borderPaddingHeight);
			
			// Draw the min and max values for x and y axis
			g.drawString("" + minY, (maxSizeLabel-minYStr.length())*TEXT_CHAR_WIDTH, 
						this.getHeight()-borderPaddingHeight);
			g.drawString("" + maxY, 0, TEXT_CHAR_HEIGHT + 1);
			
			if (this.minXValue == Integer.MAX_VALUE) {
				this.minXValue = 0;
			}
			g.drawString(this.trimDecimals(1, "" + this.minXValue), maxSizeLabel*TEXT_CHAR_WIDTH, this.getHeight());

			if (displayExtraXVals) {
				if (this.maxXValue == Integer.MIN_VALUE) {
					this.maxXValue = 0;
				}
				String maxXValueStr = this.trimDecimals(1, "" + this.maxXValue);
				g.drawString(maxXValueStr, this.getWidth()- maxXValueStr.length()*TEXT_CHAR_WIDTH, this.getHeight());
			}
			
			// Go though each data label and draw	
			for (String label : data.keySet()) {
				ArrayList<Point2D> currDataSet = data.get(label);
				if (currDataSet.size() == 0) {
					continue;
				}
				
				g.setColor(lineColours.get(label));
				// Draw the y values of the points at the y-axis
				Point2D leastRecentPt = this.axisCoordToDrawCoord(currDataSet.get(0));
				String leastRPtYVal;
				double tempValueY =	currDataSet.get(0).getY();
				int specialShiftDecimal = 0;
				double fracOfTmpY = Math.abs(tempValueY)-Math.abs((int)tempValueY);
				if ((fracOfTmpY < 1) && fracOfTmpY > EPSILON) {
					leastRPtYVal = this.trimDecimals(2, "" + tempValueY);
					specialShiftDecimal = leastRPtYVal.length();
				}
				else {
					leastRPtYVal = "" + (int)tempValueY;
				}
				
				g.drawString(leastRPtYVal, (specialShiftDecimal+maxSizeLabel-leastRPtYVal.length())*TEXT_CHAR_WIDTH, (int)leastRecentPt.getY());
				
				// Draw lines between the points
				// Use transparency..
				Graphics2D g2d = (Graphics2D)g;
				g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
				for (int i = 1; i < currDataSet.size(); i++) {
					Point2D prevPt = this.axisCoordToDrawCoord(currDataSet.get(i-1));
					Point2D currPt = this.axisCoordToDrawCoord(currDataSet.get(i));
					g.drawLine((int)prevPt.getX(), (int)prevPt.getY(), (int)currPt.getX(), (int)currPt.getY());
				}
				g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
				
				// Draw each of the data points
				for (int i = 0; i < currDataSet.size(); i++) {
					Point2D currPt = this.axisCoordToDrawCoord(currDataSet.get(i));
					g.setColor(Color.BLACK);
					g.fillOval((int)currPt.getX()-POINT_OUTLINE_RADIUS, (int)currPt.getY()-POINT_OUTLINE_RADIUS, 2*POINT_OUTLINE_RADIUS, 2*POINT_OUTLINE_RADIUS);
					g.setColor(pointColours.get(label));
					g.fillOval((int)currPt.getX()-POINT_RADIUS, (int)currPt.getY()-POINT_RADIUS, 2*POINT_RADIUS, 2*POINT_RADIUS);
				}
			}
		}
		
		/**
		 * Maps a given point from the axis coordinate system to the draw coordinate system
		 * @param p Point to transform.
		 * @return transformed point (now in draw coordinates).
		 */
		private Point2D axisCoordToDrawCoord(Point2D p) {
			// Place the point on the graph according to the value of the coordinates
			// based on the max and min values for both x and y.

			// Obtain the percentage along the time/x-axis
			double xPercent = (p.getX() - this.minXValue) / (double)(this.maxXValue - this.minXValue);
			// Obtain the percentage along the y axis that the current point is located
			double yPercent = (p.getY() - (double)minY) / (double)(maxY - minY);

			return new Point2D.Double(xPercent*(this.getWidth()-borderPaddingWidth) + borderPaddingWidth, this.getHeight() - borderPaddingHeight - yPercent*(this.getHeight()-borderPaddingHeight));
		}
		
	}
	
}
